function GUASS (x: number): number {
  return Math.exp(- x * x / 2);
}

function _GUASS (x: number): number {
  return - x * GUASS(x);
}

function SUM (inputs: number[], weights: number[]): number {
  let result = 0;
  inputs.forEach((input, j) => {
    result += input * weights[j];
  });
  return result;
}

interface IPoint {
  x: number;
  y: number;
}

class Layer {
  constructor (
    private readonly inputs: number[],
    private readonly weights: number[][],
    private readonly outputs: number[],
  ) {
    this.sums = outputs.map(() => 0);
    this.dH_dOutputs = outputs.map(() => 0);
    this.dH_dInputs = inputs.map(() => 0);
    this.dH_dWeights = weights.map(o => o.map(() => 0));
  }

  private sums: number[];
  private dH_dOutputs: number[]; // h对output的偏导 等于下一层的dH_dInputs
  private dH_dInputs: number[]; // h对input的偏导
  private dH_dWeights: number[][]; // h对w的偏导

  get_dH_dWeights (): number[][] {
    return this.dH_dWeights.map(o => [...o]);
  }

  updateOutputs () {
    // console.log(this.weights.toString());
    for (let j = 0; j < this.outputs.length; j++) {
      const sum = SUM(this.inputs, this.weights[j]);
      this.sums[j] = sum;
      this.outputs[j] = GUASS(sum);
    }
    // console.log(this.sums.toString());
    // console.log(this.outputs.toString());
  }

  updatePartials (dH_dOutputs: number[]) {
    this.dH_dOutputs = dH_dOutputs;
    for (let i = 0; i < this.inputs.length; i++) {
      this.dH_dInputs[i] = this.dH_dInput(i);
    }
    for (let j = 0; j < this.outputs.length; j++) {
      this.dH_dWeights[j] = [];
      for (let i = 0; i < this.inputs.length; i++) {
        this.dH_dWeights[j][i] = this.dH_dOutputs[j] * this.dOutput_dw(i, j);
      }
    }
    // console.log('dH_dOutputs', this.dH_dOutputs.toString());
    // console.log('dH_dInputs', this.dH_dInputs.toString());
    // console.log('dH_dWeights', this.dH_dWeights.toString());
    
  }

  private dOutput_dInput (i: number, j: number): number { //第j个output对第i个input的偏导 
    // output[j] = guass( SUM[ input[i] * weights[j][i] ] )
    // dOutput[j]/dInput[i] = -sum * guass(sum) * weights[j][i] = -sum * output[j] * weights[j][i]
    return - this.sums[j] * this.outputs[j] * this.weights[j][i];
  }

  private dOutput_dw (i: number, j: number): number { //第j个output对第j个节点的第i个参数的偏导 
    // output[j] = guass( SUM[ input[i] * weights[j][i] ] )
    // dOutput[j]/dWeight[j][i] = -sum * guass(sum) * dSum/dWeight[j][i] = -sum * output[j] * input[i]
    return - this.sums[j] * this.outputs[j] * this.inputs[i];
  }

  private dH_dInput (i: number): number { //h对第i个input的偏导
    // dH/dInput[i] = SUM[ dH/dOutput[j] * dOutput[j] / dInput[i] ]
    let result = 0;
    for (let j = 0; j < this.outputs.length; j++) {
      result += this.dH_dOutputs[j] * this.dOutput_dInput(i, j);
    }
    return result;
  }
}

class OutputLayer {
  constructor (
    private readonly inputs: number[],
    private readonly weights: number[],
    private readonly b: number[],
  ) {
    this.dH_dInputs = inputs.map(() => 0);
    this.dH_dWeights = weights.map(() => 0);
  }

  private output: number;
  private dH_dInputs: number[]; // h对input的偏导
  private dH_dWeights: number[]; // h对w的偏导
  private y: number;
  private sum: number;
  private dH_db: number;

  getOutput (): number {
    return this.output;
  }

  getSum (): number {
    return this.sum;
  }

  setY (y: number) {
    this.y = y;
  }

  get_dH_dInputs (): number[] {
    return this.dH_dInputs;
  }

  get_dH_dWeights (): number[] {
    return [ ...this.dH_dWeights ];
  }

  get_dH_db (): number {
    return this.dH_db;
  }

  updateOutput () {
    // console.log(this.inputs.toString(), this.weights.toString());
    // console.log(this.b);
    // console.log(this.inputs.toString());
    // console.log(this.weights.toString());
    this.sum = SUM(this.inputs, this.weights) + this.b[0];
    // console.log(this.sum);
    // console.log(this.sum);
    this.output = .5 * Math.pow(this.sum - this.y, 2);
    // console.log('inputs', this.inputs.toString());
    // console.log('weights', this.weights.toString());
    // console.log('sum', this.sum);
    // console.log('output', this.output);
  }

  updatePartials () {
    for (let i = 0; i < this.inputs.length; i++) {
      this.dH_dInputs[i] = this.dOutput_dInput(i);
    }
    for (let i = 0; i < this.inputs.length; i++) {
      this.dH_dWeights[i] = this.dOutput_dw(i);
    }
    this.dH_db = this.dOutput_db();
    // console.log('dH_db', this.dH_db);
    // console.log('dH_dInputs', this.dH_dInputs.toString());
    // console.log('dH_dWeights', this.dH_dWeights.toString());
  }

  dOutput_dInput (i: number): number { // output对第i个input的偏导 
    // output = 0.5 * (sum - y) ^ 2 + m * SUM(wi)
    // dOutput/dInput[i] = (sum - y) * dSum/dInput[i] + m * dSUM(wi)/dInput[i]
    //                   = (sum - y) * weights[i]
    return (this.sum - this.y) * this.weights[i];
  }

  dOutput_dw (i: number): number { //output对第i个参数的偏导 
    // output = 0.5 * (sum - y) ^ 2 + m * SUM(wi)
    // dOutput/dWeights[i] = (sum - y) * dSum/dWeights[i] + m * dSUM(wi)/dWeights[i]
    //                   = (sum - y) * inputs[i] + m
    return (this.sum - this.y) * this.inputs[i];
  }

  dOutput_db (): number { //output对第i个参数的偏导 
    // output = 0.5 * (sum - y) ^ 2
    // dOutput/db = (sum - y) * dSum/db
    //            = (sum - y)
    return (this.sum - this.y);
  }
}

export class System {
  constructor (
    private readonly points: IPoint[],
  ) {}

  private layerWeights: number[][] = [
    [1, 1],
    [1, 1],
    [1, 1],
    [1, 1],
    [1, 1],
    [1, 1],
    // [1, 1],
    // [1, 1],
    // [1, 1],
  ];
  private layerInputs: number[] = [0, 0];
  private layerOutputs: number[] = this.layerWeights.map(() => 0);
  private outputLayerWeights: number[] = this.layerOutputs.map(() => 1);
  private b = [1];

  private layer: Layer = new Layer(
    this.layerInputs,
    this.layerWeights,
    this.layerOutputs,
  );
  private outputLayer: OutputLayer = new OutputLayer(
    this.layerOutputs,
    this.outputLayerWeights,
    this.b,
  );
  private step = 0.01;

  private _x = 0;
  private _a = 1;
  private _y = 0;
  private _b = 1
  private r = 1;
  private trans (points: IPoint[]): IPoint[] {
    let maxX = Math.max(...points.map(o => o.x));
    let minX = Math.min(...points.map(o => o.x));
    let maxY = Math.max(...points.map(o => o.y));
    let minY = Math.min(...points.map(o => o.y));
    this._x = minX;
    this._y = minY;
    this._a = (maxX - minX) / 100;
    this._b = (maxY - minY) / 100;
    
    // maxX - minX -> 100
    // maxY - minY -> 100
    return points.map(p => ({
      x: (p.x - this._x) / this._a,
      y: (p.y - this._y) / this._b
    }));
  }
  
  run () {
    // const points = this.points; // this.trans(this.points);
    const points = this.trans(this.points);
    console.log(points);
    for (let l = 0; l < this.layerWeights.length; l++) {
      this.layerWeights[l] = [ -points[l].x / 50, 1 / 50];
    }
    for (let t = 0; t < 1000; t++) {
      const layer_dH_dWeights: number[][][] = [];
      const output_dH_dWeights: number[][] = [];
      const output_dH_dbs: number[] = [];
      let o = 0;
      for (let k = 0; k < points.length; k++) {
        const point = points[k];
        this.layerInputs[0] = 1;
        this.layerInputs[1] = point.x;
        this.outputLayer.setY(point.y);
        
        this.layer.updateOutputs();
        this.outputLayer.updateOutput();
  
        this.outputLayer.updatePartials();
        this.layer.updatePartials(this.outputLayer.get_dH_dInputs());
  
        layer_dH_dWeights[k] = this.layer.get_dH_dWeights();
        // console.log(layer_dH_dWeights[k]);
        output_dH_dWeights[k] = this.outputLayer.get_dH_dWeights();
        // console.log(output_dH_dWeights[k]);
        output_dH_dbs[k] = this.outputLayer.get_dH_db();
        // console.log(output_dH_dbs[k]);
        o += this.outputLayer.getOutput();
      }
      // if (o < 1) {
      //   // console.log(o);
      //   break;
      // }
      // o = SUM[ ouput[k] ]
      // do/dw = SUM[ doutput[k]/dw ]
      console.log('===>', o);
      for (let k = 1; k < layer_dH_dWeights.length; k++) {
        for (let i = 0; i < layer_dH_dWeights[k].length; i++) {
          for (let j = 0; j < layer_dH_dWeights[k][i].length; j++) {
            layer_dH_dWeights[0][i][j] += layer_dH_dWeights[k][i][j];
          }
        }
      }
      
      for (let k = 1; k < output_dH_dWeights.length; k++) {
        for (let i = 0; i < output_dH_dWeights[k].length; i++) {
          output_dH_dWeights[0][i] += output_dH_dWeights[k][i];
        }
      }
      
      for (let k = 1; k < output_dH_dbs.length; k++) {
        output_dH_dbs[0] += output_dH_dbs[k];
      }
  
      // 更新全部参数
      const n = this.layerWeights.length * this.layerWeights[0].length + this.outputLayerWeights.length;
      for (let i = 0; i < this.layerWeights.length; i++) {
        for (let j = 0; j < this.layerWeights[i].length; j++) {
          // console.log(layer_dH_dWeights[0][i][j]);
          this.layerWeights[i][j] -= this.step * (layer_dH_dWeights[0][i][j] + this.r / n * this.layerWeights[i][j]);
        }
      }
      for (let i = 0; i < this.outputLayerWeights.length; i++) {
        this.outputLayerWeights[i] -= this.step * (output_dH_dWeights[0][i] + this.r / n * this.outputLayerWeights[i]);
      }
      this.b[0] -= this.step * output_dH_dbs[0];

      // console.log(layer_dH_dWeights);
      // console.log(output_dH_dWeights);
      // console.log(output_dH_dbs);
      // console.log('new ws', this.layerWeights.toString());
      // console.log('new ws', this.outputLayerWeights.toString());
      // console.log('new b', this.b.toString());
    }
    console.log('new ws', this.layerWeights.toString());
    // console.log('new ws', this.layerWeights.map(w => - w[0] * this._a + this._x));
    console.log('new ws', this.outputLayerWeights.toString());
    console.log('new b', this.b.toString());
    console.log('_a', this._a);
    console.log('_x', this._x);
    console.log('_b', this._b);
    console.log('_y', this._y);
  }

  getFn (): (x: number) => number {
    return (x) => {
      // console.log(x);
      this.layerInputs[1] = (x - this._x) / this._a;
      this.layer.updateOutputs();
      this.outputLayer.updateOutput();
      let y = this.outputLayer.getSum();
      // console.log(y);
      // console.log(this.outputLayer.getOutput());
      return y * this._b + this._y;
    }
  }
}

